iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 23 篇

[Day 23] useIntersectionObserver

  • 分享至 

  • xImage
  •  

官方 Demo:https://vueuse.org/core/useIntersectionObserver/#useintersectionobserver

IntersectionObserver Web API

這個原始碼的核心是 IntersectionObserver,主要可以透過這個 Web API 來得知目標元素是否出現在 root 的可視範圍中,以及在可視範圍中的狀態。因為是非同步執行,可以避免在主執行序上做偵測與計算。

參數部分提幾個等等會用到的:

  • callback:當目標元素可視區域時,要執行的 callback function。
  • options
    • root:指定根元素,預設值為瀏覽器 view port,以 Demo 為例,root 就是外層那個 scroll container。
    • rootMargin:類似 CSS margin,用於擴大或縮小根元素的有效範圍。以 Demo 為例,如果 rootMargin 設定 100px,那在元素進入可視區域之前就會被檢測到。
    • threshold:目標元素可視區域達到什麼比例時觸發 callback function。
      預設值為 0.1,也就是 target 的 10% 進入可視區域時會觸發 callback function。
      也可以是 array,例如傳入 [0, 0.5, 1] 的話,在 callback function 中拿到 entry.intersectionRatio 判斷當前的比例,可以做更細緻的效果。

細節可參考 mdn:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

useIntersectionObserver 參數

  • target:要偵測的目標元素,可以是 Array。
  • callback:同 IntersectionObserver Web API。
  • options:
    • root:同 IntersectionObserver Web API。
    • rootMargin:同 IntersectionObserver Web API。
    • threshold:同 IntersectionObserver Web API。
    • window:瀏覽器環境預設為 window。
    • immediate:是否要馬上呼叫 IntersectionObserver Web API 對 target 進行觀察。

useIntersectionObserver 原始碼

// src/compositions/useIntersectionObserver.js

import { computed, ref, watch } from 'vue'
import { defaultWindow, noop, notNullish, toValue, unrefElement } from '@/helper'
import { tryOnScopeDispose } from '@/utils/shared'
import { useSupported } from '@/compositions/useSupported'

export function useIntersectionObserver(target, callback, options = {}) {
  const {
    root,
    rootMargin = '0px',
    threshold = 0.1,
    window = defaultWindow,
    immediate = true,
  } = options

  const isSupported = useSupported(() => window && 'IntersectionObserver' in window)
  const targets = computed(() => {
    const _target = toValue(target)
    return (Array.isArray(_target) ? _target : [_target]).map(unrefElement).filter(notNullish)
  })

  let cleanup = noop
  const isActive = ref(immediate)

  const stopWatch = isSupported.value
    ? watch(
      () => [targets.value, unrefElement(root), isActive.value],
      ([targets, root]) => {
        cleanup()
        if (!isActive.value)
          return

        if (!targets.length)
          return

        const observer = new IntersectionObserver(
          callback,
          {
            root: unrefElement(root),
            rootMargin,
            threshold,
          },
        )

        targets.forEach(el => el && observer.observe(el))

        cleanup = () => {
          observer.disconnect()
          cleanup = noop
        }
      },
      { immediate, flush: 'post' },
    )
    : noop

  const stop = () => {
    cleanup()
    stopWatch()
    isActive.value = false
  }

  tryOnScopeDispose(stop)

  return {
    isSupported,
    isActive,
    pause() {
      cleanup()
      isActive.value = false
    },
    resume() {
      isActive.value = true
    },
    stop,
  }
}

這個 API 原始碼的結構跟之前 Day8 ~ Day10 看到的 useEventListener 有點相似,Day8 也有提到,在組件註銷前需要透過 tryOnScopeDispose(stop) 來 stopWatch 的部分,這裡就先不多談。

先看 isSupported,瀏覽器不支援 IntersectionObserver 的話就不用玩了 XD,到 caniuse 會發現主流瀏覽器都有支援,所以可以放心使用。

核心邏輯在 watch 內,來看看有哪些方式可以觸發這個 watch callback function。

  1. immediate 為 true,組件掛載後馬上執行。
  2. immediate 為 false,因為有 return resume 出去,在外層執行 resume 時,isActive 這個 ref 會變成 true,isActive 是這個 watch 觀察的物件,所以變化的時候會執行 watch callback function。

要如何暫停 IntersectionObserver API 的觀察?return 出去的物件中有一個 pause:

pause() {
  cleanup()
  isActive.value = false
},

執行 cleanup 的時候,會透過 observer.disconnect() 來執行 IntersectionObserver API 的 disconnect 方法,這招就可以停止觀察。

接著來看核心:

// src/compositions/useIntersectionObserver.js
// ...略

const observer = new IntersectionObserver(
  callback,
  {
    root: unrefElement(root),
    rootMargin,
    threshold,
  },
)

targets.forEach(el => el && observer.observe(el))

// ...略

使用 IntersectionObserver Web API 來偵測 target(s) 在 root 元素中的狀態,target(s) 進到可視範圍中,觸發我們傳入的 callback。
來看看 Demo code 怎麼使用 callback 的:

<!-- src/components/UseIntersectionObserverDemo.vue -->

<script setup>
// ...略

const { isActive, pause, resume } = useIntersectionObserver(
  target,
  ([{ isIntersecting }]) => {
    isVisible.value = isIntersecting
  },
  { root },
)
</script>

可以看到 callback 中透過解構拿到 IntersectionObserver Web API 給我們的 isIntersecting,isIntersecting 為 true 代表 target 進到可視範圍中。
callback function 的其他參數們可以參考 mdn:https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting

整個流程差不多就是這樣,看完覺得 IntersectionObserver Web API 真的是個厲害的角色,vueuse 也把它封裝成更方便使用的版本,如果要做更細緻的功能,也可以透過 callback function 拿到更多 IntersectionObserver Web API 參數來處理,滿喜歡這種設計。

GitHub PR:https://github.com/RhinoLee/30days_vue/pull/22/files


useIntersectionObserver API 就到這邊告一段落啦~
明天繼續看 useElementVisibility API,其實本來今天就要看了 XD 只是突然發現原來 useElementVisibility 的核心是 useIntersectionObserver。


上一篇
[Day 22] useScroll - scrolling
下一篇
[Day 24] useElementVisibility
系列文
30 天 vueuse 原始碼閱讀與實作 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言